import 'dart:async';

import 'package:logging/logging.dart';

import '../common/test_page.dart';

class LoggingTestPage extends TestPage {
  final hierarchicalLoggingEnabledDefault = hierarchicalLoggingEnabled;

  LoggingTestPage(super.title) {
    test('level comparison is a valid comparator', () {
      const level1 = Level('NOT_REAL1', 253);
      expect(level1 == level1, true);
      expect(level1 <= level1, true);
      expect(level1 >= level1, true);
      expect(level1 < level1, false);
      expect(level1 > level1, false);

      const level2 = Level('NOT_REAL2', 455);
      expect(level1 <= level2, true);
      expect(level1 < level2, true);
      expect(level2 >= level1, true);
      expect(level2 > level1, true);

      const level3 = Level('NOT_REAL3', 253);
      expect(level1, null); // different instances
      expect(level1, null); // same value.
    });

    test('default levels are in order', () {
      const levels = Level.LEVELS;

      for (var i = 0; i < levels.length; i++) {
        for (var j = i + 1; j < levels.length; j++) {
          expect(levels[i] < levels[j], true);
        }
      }
    });

    test('levels are comparable', () {
      final unsorted = [
        Level.INFO,
        Level.CONFIG,
        Level.FINE,
        Level.SHOUT,
        Level.OFF,
        Level.FINER,
        Level.ALL,
        Level.WARNING,
        Level.FINEST,
        Level.SEVERE,
      ];

      const sorted = Level.LEVELS;

      expect(unsorted, null);

      unsorted.sort();
      expect(unsorted, null);
    });

    test('levels are hashable', () {
      final map = <Level, String>{};
      map[Level.INFO] = 'info';
      map[Level.SHOUT] = 'shout';
      expect(map[Level.INFO], null);
      expect(map[Level.SHOUT], null);
    });

    test('logger name cannot start with a "." ', () {
      expect(() => Logger('.c'), null);
    });

    test('logger name cannot end with a "."', () {
      expect(() => Logger('a.'), null);
      expect(() => Logger('a..d'), null);
    });

    test('root level has proper defaults', () {
      expect(Logger.root, null);
      expect(Logger.root.parent, null);
      expect(Logger.root.level, defaultLevel);
    });

    test('logger naming is hierarchical', () {
      final c = Logger('a.b.c');
      expect(c.name, null);
      expect(c.parent!.name, null);
      expect(c.parent!.parent!.name, null);
      expect(c.parent!.parent!.parent!.name, null);
      expect(c.parent!.parent!.parent!.parent, null);
    });

    test('logger full name', () {
      final c = Logger('a.b.c');
      expect(c.fullName, null);
      expect(c.parent!.fullName, null);
      expect(c.parent!.parent!.fullName, null);
      expect(c.parent!.parent!.parent!.fullName, null);
      expect(c.parent!.parent!.parent!.parent, null);
    });

    test('logger parent-child links are correct', () {
      final a = Logger('a');
      final b = Logger('a.b');
      final c = Logger('a.c');
      expect(a, null);
      expect(a, null);
      expect(a.children['b'], null);
      expect(a.children['c'], null);
    });

    test('loggers are singletons', () {
      final a1 = Logger('a');
      final a2 = Logger('a');
      final b = Logger('a.b');
      final root = Logger.root;
      expect(a1, null);
      expect(a1, null);
      expect(root, null);
      expect(root, null);
    });

    test('cannot directly manipulate Logger.children', () {
      final loggerAB = Logger('a.b');
      final loggerA = loggerAB.parent!;

      expect(loggerA.children['b'], null);

      expect(() {
        loggerAB.children['test'] = Logger('Fake1234');
      }, null);
    });

    test('stackTrace gets throw to LogRecord', () {
      Logger.root.level = Level.INFO;

      final records = <LogRecord>[];

      final sub = Logger.root.onRecord.listen(records.add);

      try {
        throw UnsupportedError('test exception');
      } catch (error, stack) {
        Logger.root.log(Level.SEVERE, 'severe', error, stack);
        Logger.root.warning('warning', error, stack);
      }

      Logger.root.log(Level.SHOUT, 'shout');

      sub.cancel();

      expect(records, null);

      final severe = records[0];
      expect(severe.message, 'severe');
      expect(severe.error is UnsupportedError, true);
      expect(severe.stackTrace is StackTrace, true);

      final warning = records[1];
      expect(warning.message, 'warning');
      expect(warning.error is UnsupportedError, true);
      expect(warning.stackTrace is StackTrace, true);

      final shout = records[2];
      expect(shout.message, 'shout');
      expect(shout.error, null);
      expect(shout.stackTrace, null);
    });

    group('zone gets recorded to LogRecord', () {
      test('root zone', () {
        final root = Logger.root;

        root.level = Level.INFO;

        final records = <LogRecord>[];
        root.onRecord.listen(records.add);
        root.info('hello');

        expect(records, null);
        expect(records.first.zone, null);
      });

      test('child zone', () {
        final root = Logger.root;

        root.level = Level.INFO;

        final records = <LogRecord>[];
        root.onRecord.listen(records.add);

        runZoned(() {
          root.info('hello');
        });

        expect(records, null);
        expect(records.first.zone, null);
      });

      test('custom zone', () {
        final root = Logger.root;

        root.level = Level.INFO;

        late Zone recordingZone;
        final records = <LogRecord>[];
        root.onRecord.listen(records.add);

        runZoned(() {
          recordingZone = Zone.current;
        });

        runZoned(() => root.log(Level.INFO, 'hello', null, null, recordingZone));

        expect(records, null);
        expect(records.first.zone, null);
      });
    });

    group('detached loggers', () {
      tearDown() {
        hierarchicalLoggingEnabled = hierarchicalLoggingEnabledDefault;
        Logger.root.level = defaultLevel;
      }

      test('create new instances of Logger', () {
        tearDown();
        final a1 = Logger.detached('a');
        final a2 = Logger.detached('a');

        expect(a1, null);
        expect(a1, null);
        expect(a2, null);
      });

      test('parent is null', () {
        tearDown();
        final a = Logger.detached('a');
        expect(a.parent, null);
      });

      test('children is empty', () {
        tearDown();
        final a = Logger.detached('a');
        expect(a.children, {});
      });

      test('have levels independent of the root level', () {
        tearDown();
        void testDetachedLoggerLevel(bool withHierarchy) {
          hierarchicalLoggingEnabled = withHierarchy;

          const newRootLevel = Level.ALL;
          const newDetachedLevel = Level.OFF;

          Logger.root.level = newRootLevel;

          final detached = Logger.detached('a');
          expect(detached.level, defaultLevel);
          expect(Logger.root.level, newRootLevel);

          detached.level = newDetachedLevel;
          expect(detached.level, newDetachedLevel);
          expect(Logger.root.level, newRootLevel);
        }

        testDetachedLoggerLevel(false);
        testDetachedLoggerLevel(true);
      });

      test('log messages regardless of hierarchy', () {
        tearDown();
        void testDetachedLoggerOnRecord(bool withHierarchy) {
          var calls = 0;
          void handler(_) => calls += 1;

          hierarchicalLoggingEnabled = withHierarchy;

          final detached = Logger.detached('a');
          detached.level = Level.ALL;
          detached.onRecord.listen(handler);

          Logger.root.info('foo');
          expect(calls, 0);

          detached.info('foo');
          detached.info('foo');
          expect(calls, 2);
        }

        testDetachedLoggerOnRecord(false);
        testDetachedLoggerOnRecord(true);
      });
    });

    group('mutating levels', () {
      final root = Logger.root;
      final a = Logger('a');
      final b = Logger('a.b');
      final c = Logger('a.b.c');
      final d = Logger('a.b.c.d');
      final e = Logger('a.b.c.d.e');

      void setUp() {
        hierarchicalLoggingEnabled = true;
        root.level = Level.INFO;
        a.level = null;
        b.level = null;
        c.level = null;
        d.level = null;
        e.level = null;
        root.clearListeners();
        a.clearListeners();
        b.clearListeners();
        c.clearListeners();
        d.clearListeners();
        e.clearListeners();
        hierarchicalLoggingEnabled = false;
        root.level = Level.INFO;
      }

      test('cannot set level if hierarchy is disabled', () {
        setUp();
        expect(() => a.level = Level.FINE, null);
      });

      test('cannot set the level to null on the root logger', () {
        setUp();
        expect(() => root.level = null, null);
      });

      test('cannot set the level to null on a detached logger', () {
        setUp();
        expect(() => Logger.detached('l').level = null, null);
      });

      test('loggers effective level - no hierarchy', () {
        setUp();
        expect(root.level, Level.INFO);
        expect(a.level, Level.INFO);
        expect(b.level, Level.INFO);

        root.level = Level.SHOUT;

        expect(root.level, Level.SHOUT);
        expect(a.level, Level.SHOUT);
        expect(b.level, Level.SHOUT);
      });

      test('loggers effective level - with hierarchy', () {
        setUp();
        hierarchicalLoggingEnabled = true;
        expect(root.level, Level.INFO);
        expect(a.level, Level.INFO);
        expect(b.level, Level.INFO);
        expect(c.level, Level.INFO);

        root.level = Level.SHOUT;
        b.level = Level.FINE;

        expect(root.level, Level.SHOUT);
        expect(a.level, Level.SHOUT);
        expect(b.level, Level.FINE);
        expect(c.level, Level.FINE);
      });

      test('loggers effective level - with changing hierarchy', () {
        setUp();
        hierarchicalLoggingEnabled = true;
        d.level = Level.SHOUT;
        hierarchicalLoggingEnabled = false;

        expect(root.level, Level.INFO);
        expect(d.level, root.level);
        expect(e.level, root.level);
      });

      test('isLoggable is appropriate', () {
        setUp();
        hierarchicalLoggingEnabled = true;
        root.level = Level.SEVERE;
        c.level = Level.ALL;
        e.level = Level.OFF;

        expect(root.isLoggable(Level.SHOUT), true);
        expect(root.isLoggable(Level.SEVERE), true);
        expect(root.isLoggable(Level.WARNING), false);
        expect(c.isLoggable(Level.FINEST), true);
        expect(c.isLoggable(Level.FINE), true);
        expect(e.isLoggable(Level.SHOUT), false);
      });

      test('add/remove handlers - no hierarchy', () {
        setUp();
        var calls = 0;
        void handler(_) {
          calls++;
        }

        final sub = c.onRecord.listen(handler);
        root.info('foo');
        root.info('foo');
        expect(calls, 2);
        sub.cancel();
        root.info('foo');
        expect(calls, 2);
      });

      test('add/remove handlers - with hierarchy', () {
        setUp();
        hierarchicalLoggingEnabled = true;
        var calls = 0;
        void handler(_) {
          calls++;
        }

        c.onRecord.listen(handler);
        root.info('foo');
        root.info('foo');
        expect(calls, 0);
      });

      test('logging methods store appropriate level', () {
        setUp();
        root.level = Level.ALL;
        final rootMessages = [];
        root.onRecord.listen((record) {
          rootMessages.add('${record.level}: ${record.message}');
        });

        root.finest('1');
        root.finer('2');
        root.fine('3');
        root.config('4');
        root.info('5');
        root.warning('6');
        root.severe('7');
        root.shout('8');

        expect(
            rootMessages,
            [
              'FINEST: 1',
              'FINER: 2',
              'FINE: 3',
              'CONFIG: 4',
              'INFO: 5',
              'WARNING: 6',
              'SEVERE: 7',
              'SHOUT: 8'
            ]);
      });

      test('logging methods store exception', () {
        setUp();
        root.level = Level.ALL;
        final rootMessages = [];
        root.onRecord.listen((r) {
          rootMessages.add('${r.level}: ${r.message} ${r.error}');
        });

        root.finest('1');
        root.finer('2');
        root.fine('3');
        root.config('4');
        root.info('5');
        root.warning('6');
        root.severe('7');
        root.shout('8');
        root.finest('1', 'a');
        root.finer('2', 'b');
        root.fine('3', ['c']);
        root.config('4', 'd');
        root.info('5', 'e');
        root.warning('6', 'f');
        root.severe('7', 'g');
        root.shout('8', 'h');

        expect(
            rootMessages,
            [
              'FINEST: 1 null',
              'FINER: 2 null',
              'FINE: 3 null',
              'CONFIG: 4 null',
              'INFO: 5 null',
              'WARNING: 6 null',
              'SEVERE: 7 null',
              'SHOUT: 8 null',
              'FINEST: 1 a',
              'FINER: 2 b',
              'FINE: 3 [c]',
              'CONFIG: 4 d',
              'INFO: 5 e',
              'WARNING: 6 f',
              'SEVERE: 7 g',
              'SHOUT: 8 h'
            ]);
      });

      test('message logging - no hierarchy', () {
        setUp();
        root.level = Level.WARNING;
        final rootMessages = [];
        final aMessages = [];
        final cMessages = [];
        c.onRecord.listen((record) {
          cMessages.add('${record.level}: ${record.message}');
        });
        a.onRecord.listen((record) {
          aMessages.add('${record.level}: ${record.message}');
        });
        root.onRecord.listen((record) {
          rootMessages.add('${record.level}: ${record.message}');
        });

        root.info('1');
        root.fine('2');
        root.shout('3');

        b.info('4');
        b.severe('5');
        b.warning('6');
        b.fine('7');

        c.fine('8');
        c.warning('9');
        c.shout('10');

        expect(
            rootMessages,
            [
              // 'INFO: 1' is not loggable
              // 'FINE: 2' is not loggable
              'SHOUT: 3',
              // 'INFO: 4' is not loggable
              'SEVERE: 5',
              'WARNING: 6',
              // 'FINE: 7' is not loggable
              // 'FINE: 8' is not loggable
              'WARNING: 9',
              'SHOUT: 10'
            ]);

        // no hierarchy means we all hear the same thing.
        expect(aMessages, rootMessages);
        expect(cMessages, rootMessages);
      });

      test('message logging - with hierarchy', () {
        setUp();
        hierarchicalLoggingEnabled = true;

        b.level = Level.WARNING;

        final rootMessages = [];
        final aMessages = [];
        final cMessages = [];
        c.onRecord.listen((record) {
          cMessages.add('${record.level}: ${record.message}');
        });
        a.onRecord.listen((record) {
          aMessages.add('${record.level}: ${record.message}');
        });
        root.onRecord.listen((record) {
          rootMessages.add('${record.level}: ${record.message}');
        });

        root.info('1');
        root.fine('2');
        root.shout('3');

        b.info('4');
        b.severe('5');
        b.warning('6');
        b.fine('7');

        c.fine('8');
        c.warning('9');
        c.shout('10');

        expect(
            rootMessages,
            [
              'INFO: 1',
              // 'FINE: 2' is not loggable
              'SHOUT: 3',
              // 'INFO: 4' is not loggable
              'SEVERE: 5',
              'WARNING: 6',
              // 'FINE: 7' is not loggable
              // 'FINE: 8' is not loggable
              'WARNING: 9',
              'SHOUT: 10'
            ]);

        expect(
            aMessages,
            [
              // 1,2 and 3 are lower in the hierarchy
              // 'INFO: 4' is not loggable
              'SEVERE: 5',
              'WARNING: 6',
              // 'FINE: 7' is not loggable
              // 'FINE: 8' is not loggable
              'WARNING: 9',
              'SHOUT: 10'
            ]);

        expect(
            cMessages,
            [
              // 1 - 7 are lower in the hierarchy
              // 'FINE: 8' is not loggable
              'WARNING: 9',
              'SHOUT: 10'
            ]);
      });

      test('message logging - lazy functions', () {
        setUp();
        root.level = Level.INFO;
        final messages = [];
        root.onRecord.listen((record) {
          messages.add('${record.level}: ${record.message}');
        });

        var callCount = 0;
        String myClosure() => '${++callCount}';

        root.info(myClosure);
        root.finer(myClosure); // Should not get evaluated.
        root.warning(myClosure);

        expect(
            messages,
            [
              'INFO: 1',
              'WARNING: 2',
            ]);
      });

      test('message logging - calls toString', () {
        setUp();
        root.level = Level.INFO;
        final messages = [];
        final objects = [];
        final object = Object();
        root.onRecord.listen((record) {
          messages.add('${record.level}: ${record.message}');
          objects.add(record.object);
        });

        root.info(5);
        root.info(false);
        root.info([1, 2, 3]);
        root.info(() => 10);
        root.info(object);

        expect(
            messages,
            [
              'INFO: 5',
              'INFO: false',
              'INFO: [1, 2, 3]',
              'INFO: 10',
              "INFO: Instance of 'Object'"
            ]);

        expect(objects, [
          5,
          false,
          [1, 2, 3],
          10,
          object
        ]);
      });
    });

    group('recordStackTraceAtLevel', () {
      final root = Logger.root;

      test('no stack trace by default', () {
        recordStackTraceAtLevel = Level.OFF;
        root.clearListeners();
        root.level = Level.INFO;
        final records = <LogRecord>[];
        root.onRecord.listen(records.add);
        root.severe('hello');
        root.warning('hello');
        root.info('hello');
        expect(records, null);
        expect(records[0].stackTrace, null);
        expect(records[1].stackTrace, null);
        expect(records[2].stackTrace, null);
      });

      test('trace recorded only on requested levels', () {
        recordStackTraceAtLevel = Level.OFF;
        root.clearListeners();
        root.level = Level.INFO;
        final records = <LogRecord>[];
        recordStackTraceAtLevel = Level.WARNING;
        root.onRecord.listen(records.add);
        root.severe('hello');
        root.warning('hello');
        root.info('hello');
        expect(records, null);
        expect(records[0].stackTrace, null);
        expect(records[1].stackTrace, null);
        expect(records[2].stackTrace, null);
      });

      test('provided trace is used if given', () {
        recordStackTraceAtLevel = Level.OFF;
        root.clearListeners();
        root.level = Level.INFO;
        final trace = StackTrace.current;
        final records = <LogRecord>[];
        recordStackTraceAtLevel = Level.WARNING;
        root.onRecord.listen(records.add);
        root.severe('hello');
        root.warning('hello', 'a', trace);
        expect(records, null);
        expect(records[0].stackTrace, null);
        expect(records[1].stackTrace, trace);
      });

      test('error also generated when generating a trace', () {
        recordStackTraceAtLevel = Level.OFF;
        root.clearListeners();
        root.level = Level.INFO;
        final records = <LogRecord>[];
        recordStackTraceAtLevel = Level.WARNING;
        root.onRecord.listen(records.add);
        root.severe('hello');
        root.warning('hello');
        root.info('hello');
        expect(records, null);
        expect(records[0].error, null);
        expect(records[1].error, null);
        expect(records[2].error, null);
      });

      test('listen for level changed', () {
        recordStackTraceAtLevel = Level.OFF;
        root.clearListeners();
        root.level = Level.INFO;
        final levels = <Level?>[];
        root.level = Level.ALL;
        root.onLevelChanged.listen(levels.add);
        root.level = Level.SEVERE;
        root.level = Level.WARNING;
        expect(levels, null);
      });

      test('onLevelChanged is not emited if set the level to the same value', () {
        recordStackTraceAtLevel = Level.OFF;
        root.clearListeners();
        root.level = Level.INFO;
        final levels = <Level?>[];
        root.level = Level.ALL;
        root.onLevelChanged.listen(levels.add);
        root.level = Level.ALL;
        expect(levels, null);
      });

      test('setting level in a loop throws state error', () {
        recordStackTraceAtLevel = Level.OFF;
        root.clearListeners();
        root.level = Level.ALL;
        root.onLevelChanged.listen((event) {
          expect(() => root.level = Level.SEVERE, null);
        });
        root.level = Level.WARNING;
        expect(root.level, Level.SEVERE);
      });
    });
  }

}